package com.crunch.physics;

import com.crunch.math.Vector2f;

/**
 * A proximity zone in the physics simulation.
 */
public class PhysicsProximityZone {
	public static class Description {
		public Shape shape;
		public Vector2f boundMin;   // must only be set if shape is RECTANGLE
		public Vector2f boundMax;   // must only be set if shape is RECTANGLE
		public Vector2f center;     // must only be set if shape is CIRCLE
		public float radius;        // must only be set if shape is CIRCLE
		public int categoryMask;
		public Object data;
		public EventHandler eventHandler;
	}

	public interface EventHandler {
		// called each step the zone and entity intersect
		public void onIntersect(PhysicsProximityZone proximityZone, DynamicPhysicsEntity entity);
		// called when the entity enters the zone (before onIntersect() for the step)
		public void onEnter(PhysicsProximityZone proximityZone, DynamicPhysicsEntity entity);
		// called when the entity exits the zone
		public void onExit(PhysicsProximityZone proximityZone, DynamicPhysicsEntity entity);
	}

	public enum Shape {
		RECTANGLE,
		CIRCLE
	}

	PhysicsProximityZone(PhysicsSimulation simulation, Description description) {
		if (description.shape == Shape.RECTANGLE &&
				(description.boundMax.x() < description.boundMin.x() || description.boundMax.y() < description.boundMin.y())) {
			throw new IllegalArgumentException("Invalid bounds");
		}
		if (description.shape == Shape.CIRCLE && description.radius < 0.0f) {
			throw new IllegalArgumentException("Negative radius");
		}

		this.simulation = simulation;

		shape = description.shape;
		switch (shape) {
			case RECTANGLE:
				boundMin = new Vector2f(description.boundMin);
				boundMax = new Vector2f(description.boundMax);
				center = null;
				radius = 0.0f;
				break;
			case CIRCLE:
				boundMin = new Vector2f(
						description.center.x() - description.radius,
						description.center.y() - description.radius);
				boundMax = new Vector2f(
						description.center.x() + description.radius,
						description.center.y() + description.radius);
				center = new Vector2f(description.center);
				radius = description.radius;
				break;
			default:
				throw new IllegalArgumentException("Invalid shape");
		}
		categoryMask = description.categoryMask;
		data = description.data;
		eventHandler = description.eventHandler;

		setFlag(FLAG_IN_SIMULATION, false);
		setFlag(FLAG_EXISTS, true);

		if (simulation.simulationLock) {
			simulation.proximityZoneAddQueue.add(this);
		} else {
			addToSimulation();
		}
	}

	void addToSimulation() {
		setFlag(FLAG_IN_SIMULATION, true);
		simulation.proximityZones.add(this);

		// add to dynamic grid
		Vector2f aabbMin = new Vector2f();
		Vector2f aabbMax = new Vector2f();
		getAabb(aabbMin, aabbMax, simulation.settings.contactThreshold);
		// add to hash grid - prozimity zones are not activators
		gridEntity = simulation.hashGrid.addEntity(aabbMin, aabbMax, PhysicsSimulation.GRID_PROXIMITY_ZONE_TYPE, false, this);
	}

	public void remove() {
		checkExists();
		setFlag(FLAG_EXISTS, false);

		// remove tracked intersections
		while (trackedIntersectionList != null) {
			if (!simulation.trackedIntersectionLock) {
				PhysicsSimulation.TrackedIntersection ti = new PhysicsSimulation.TrackedIntersection();
				ti.proximityZone = trackedIntersectionList.proximityZone;
				ti.dynamicEntity = trackedIntersectionList.dynamicEntity;
				simulation.trackedIntersections.remove(ti);
			}
			DynamicPhysicsEntity e = trackedIntersectionList.dynamicEntity;
			trackedIntersectionList.unlink();

			// tracked intersection should have already been destroyed if entity doesn't exist
			assert e.checkFlag(DynamicPhysicsEntity.FLAG_EXISTS);
			boolean validCategory = CategoryMask.doesMaskContainCategory(categoryMask, e.category);
			if (validCategory && eventHandler != null) {
				eventHandler.onExit(this, e);
			}
		}

		// remove immediately from the grid
		if (gridEntity != null) {
			gridEntity.remove();
			gridEntity = null;
		}

		if (simulation.simulationLock || simulation.raycastLock) {
			// simulation is locked - we can't remove directly
			// add to deletion queue if in the simulation
			if (checkFlag(FLAG_IN_SIMULATION)) {
				simulation.proximityZoneDeleteQueue.add(this);
			}
		} else {
			// simulation is not locked - we can directly modify without going through queues
			// if the simulation isn't locked, we should have added this entity by this point
			assert checkFlag(FLAG_IN_SIMULATION);
			// remove from simulation
			simulation.proximityZones.remove(this);
		}
	}

	public boolean exists() {
		return checkFlag(FLAG_EXISTS);
	}

	public void setShape(Vector2f boundMin, Vector2f boundMax) {
		checkExists();

		if (boundMax.x() < boundMin.x() || boundMax.y() < boundMin.y()) {
			throw new IllegalArgumentException("Invalid bounds");
		}

		if (shape == Shape.RECTANGLE && this.boundMin.equals(boundMin) && this.boundMax.equals(boundMax)) {
			return; // no change
		}

		this.boundMin.set(boundMin);
		this.boundMax.set(boundMax);
		center = null;
		radius = 0.0f;

		if (gridEntity != null) {
			// move in dynamic grid
			Vector2f aabbMin = new Vector2f();
			Vector2f aabbMax = new Vector2f();
			getAabb(aabbMin, aabbMax, simulation.settings.contactThreshold);
			// add to hash grid - dynamic entities are activators
			gridEntity.move(aabbMin, aabbMax);
		}
	}

	public void setShape(Vector2f center, float radius) {
		checkExists();

		if (radius < 0.0f) {
			throw new IllegalArgumentException("Negative radius");
		}

		if (shape == Shape.CIRCLE && this.center.equals(center) && this.radius == radius) {
			return; // no change
		}

		boundMin.set(
				center.x() - radius,
				center.y() - radius);
		boundMax.set(
				center.x() + radius,
				center.y() + radius);
		if (this.center == null) {
			this.center = new Vector2f(center);
		} else {
			this.center.set(center);
		}
		this.radius = radius;

		if (gridEntity != null) {
			// move in dynamic grid
			Vector2f aabbMin = new Vector2f();
			Vector2f aabbMax = new Vector2f();
			getAabb(aabbMin, aabbMax, simulation.settings.contactThreshold);
			// add to hash grid - dynamic entities are activators
			gridEntity.move(aabbMin, aabbMax);
		}
	}

	public void setCategoryMask(int categoryMask) {
		checkExists();
		this.categoryMask = categoryMask;
	}

	public void setData(Object data) {
		checkExists();
		this.data = data;
	}

	public void setEventHandler(EventHandler eventHandler) {
		checkExists();
		this.eventHandler = eventHandler;
	}

	public PhysicsSimulation getSimulation() {
		return checkFlag(FLAG_EXISTS) ? simulation : null;
	}

	public Shape getShape() {
		return shape;
	}

	public Vector2f getShapeBoundMin() {
		return new Vector2f(boundMin);
	}

	public Vector2f getShapeBoundMax() {
		return new Vector2f(boundMax);
	}

	public Vector2f getShapeCenter() {
		return (shape == Shape.CIRCLE) ? new Vector2f(center) : null;
	}

	public float getShapeRadius() {
		return radius;
	}

	public int getCategoryMask() {
		return categoryMask;
	}

	public Object getData() {
		return data;
	}

	public EventHandler getEventHandler() {
		return eventHandler;
	}

	private PhysicsSimulation simulation;

	// fields are package-private to allow access by physics simulator
	Shape shape;                // shape of the proximity zone
	Vector2f boundMin;          // min bound - if CIRCLE, equal to center - radius
	Vector2f boundMax;          // max bound - if CIRCLE, equal to center + radius
	Vector2f center;            // if CIRCLE, circle center; else null
	float radius;               // if CIRCLE, circle radius; else 0.0f
	int categoryMask;           // determines which entities can collide with this zone
	Object data;                // user-provided data
	EventHandler eventHandler;  // event handler for this entity
	HashGrid.Entity gridEntity; // entity in the hash grid
	PhysicsSimulation.TrackedIntersectionListNode trackedIntersectionList;  // list of tracked intersection nodes

	byte flags = 0;             // whether this entity is flagged for deletion
	static final byte FLAG_IN_SIMULATION    = 1;    // whether entity has been added to the simulation
	static final byte FLAG_EXISTS           = 2;    // whether entity "exists" to the user
	boolean checkFlag(byte flag) {
		return (flags & flag) != 0;
	}
	void setFlag(byte flag, boolean e) {
		flags = (byte) (e ? (flags | flag) : (flags & ~flag));
	}

	void checkExists() {
		if (!checkFlag(FLAG_EXISTS)) {
			throw new IllegalStateException("Not in simulation");
		}
	}

	void getAabb(Vector2f aabbMin, Vector2f aabbMax, float expansion) {
		aabbMin.set(boundMin);
		aabbMax.set(boundMax);
		aabbMin.x(aabbMin.x() - expansion);
		aabbMin.y(aabbMin.y() - expansion);
		aabbMax.x(aabbMax.x() + expansion);
		aabbMax.y(aabbMax.y() + expansion);
	}
}
